<!DOCTYPE html>
<html>
<head>
<title>mouse_test</title>
<script src="test_bootstrap.js"></script>
<script type="text/javascript">
  goog.require('bot.Mouse');
  goog.require('bot.action');
  goog.require('bot.userAgent');
  goog.require('goog.Promise');
  goog.require('goog.array');
  goog.require('goog.dom');
  goog.require('goog.events');
  goog.require('goog.events.EventType');
  goog.require('goog.math.Coordinate');
  goog.require('goog.style');
  goog.require('goog.testing.jsunit');
  goog.require('goog.userAgent');
</script>
<body>
<div id="green" style="background-color:green; width:100px; height:50px">
<div id="red" style="background-color:red; width:50px; height:25px;
position: relative; top:25px;">Red</div>
</div>
<div id="capture" style="background-color:blue; width:100px; height:50px">
  <div id="innerCapture" style="background-color:cyan;
                                width:50px;
                                height:25px;">
  </div>
</div>
<script type="text/javascript">
  // The goog.events.EventType enum is missing mouse scrolling event types.
  // This a little hacky but gives us some stylistic consistency :).
  goog.events.EventType.MOUSEWHEEL =
    goog.userAgent.GECKO ? 'DOMMouseScroll' : 'mousewheel';
  goog.events.EventType.MOUSEPIXELSCROLL = 'MozMousePixelScroll';

  var events = [];
  var greenDiv, redDiv, captureDiv, innerCaptureDiv;
  var MOUSE_EVENTS = [
    goog.events.EventType.MOUSEOUT,
    goog.events.EventType.MOUSEOVER,
    goog.events.EventType.MOUSEMOVE,
    goog.events.EventType.MOUSEDOWN,
    goog.events.EventType.MOUSEUP,
    goog.events.EventType.MOUSEWHEEL,
    goog.events.EventType.MOUSEPIXELSCROLL,
    goog.events.EventType.CLICK,
    goog.events.EventType.CONTEXTMENU,
    goog.events.EventType.DBLCLICK,
    goog.events.EventType.MSPOINTERDOWN,
    goog.events.EventType.MSPOINTERMOVE,
    goog.events.EventType.MSPOINTEROVER,
    goog.events.EventType.MSPOINTEROUT,
    goog.events.EventType.MSPOINTERUP
  ];

  function setUpPage() {
    greenDiv = bot.locators.findElement({id: 'green'});
    redDiv = bot.locators.findElement({id: 'red'});
    captureDiv = bot.locators.findElement({id: 'capture'});
    innerCaptureDiv = bot.locators.findElement({id: 'innerCapture'});
    goog.testing.TestCase.getActiveTestCase().promiseTimeout = 30000; // 30s
  }

  function setUp() {
    bot.getDocument().documentElement.focus();
    events = [];
    goog.events.removeAll(greenDiv);
    goog.events.removeAll(redDiv);
    goog.events.removeAll(captureDiv);
    goog.events.removeAll(innerCaptureDiv);

    goog.events.listen(greenDiv, MOUSE_EVENTS, function(e) {
      events.push(e.type);
      events.push(e.target);
      events.push(e.button);
    });

    goog.events.listen(captureDiv, MOUSE_EVENTS, function(e) {
      events.push(e.type);
      events.push(e.target);
      events.push(e.button);
      if (e.type == goog.events.EventType.MSPOINTERDOWN) {
        captureDiv.msSetPointerCapture(e.getBrowserEvent().pointerId);
      }
    });
  }

  /**
   * Returns the button value in the object depending on the current useragent.
   * Returns the 'wk' property for WebKit and IE9 in standards mode, the 'ie'
   * property for IE, and the 'ff' property for Firefox.
   */
  function b(button) {
    return bot.userAgent.IE_DOC_9 || goog.userAgent.WEBKIT ? button['wk'] :
        (goog.userAgent.IE ? button['ie'] :
            button['ff']);  // Firefox
  }

  function assertEvents(var_args) {
    var expectedEvents = goog.array.concat.apply(null, arguments);
    assertArrayEquals(expectedEvents, events);
    events = [];
  }

  function mousedownEvents(elem, button) {
     var events = [goog.events.EventType.MOUSEDOWN, elem, button];
     return !bot.userAgent.IE_DOC_10 ? events :
         [goog.events.EventType.MSPOINTERDOWN, elem, button].concat(events);
  }

  function mousemoveEvents(elem, button) {
     var events = [goog.events.EventType.MOUSEMOVE, elem, button];
     return bot.userAgent.IE_DOC_10 ?
         [goog.events.EventType.MSPOINTERMOVE, elem, -1].concat(events) :
         events;
  }

  function mouseoutEvents(elem, button) {
     var events = [goog.events.EventType.MOUSEOUT, elem, button];
     return bot.userAgent.IE_DOC_10 ?
         [goog.events.EventType.MSPOINTEROUT, elem, -1].concat(events) :
         events;
  }

  function mouseupEvents(elem, button) {
     var events = [goog.events.EventType.MOUSEUP, elem, button];
     return bot.userAgent.IE_DOC_10 ?
         [goog.events.EventType.MSPOINTERUP, elem, button].concat(events) :
         events;
  }

  function mouseoverAndMoveEvents(elem, button) {
    function mouseoverEvents(elem, button) {
       var events = [goog.events.EventType.MOUSEOVER, elem, button];
       return bot.userAgent.IE_DOC_10 ?
           [goog.events.EventType.MSPOINTEROVER, elem, -1].concat(events) :
           events;
    }
    // IE fires the movemove *before* the mouseover event, and
    // IE < 9 always supplies a mouseover button value of 0.
    return goog.userAgent.IE ?
        mousemoveEvents(elem, button).concat(
             mouseoverEvents(elem, bot.userAgent.IE_DOC_9 ? button : 0)) :
        mouseoverEvents(elem, button).concat(mousemoveEvents(elem, button));
  }

  function testNoClickWhenPressHiddenElement() {
    var coords = new goog.math.Coordinate(5, 5);
    var mouse = new bot.Mouse();
    mouse.move(greenDiv, coords);
    goog.style.showElement(greenDiv, false);
    mouse.pressButton(bot.Mouse.Button.LEFT);
    mouse.releaseButton();

    assertEvents(mouseoverAndMoveEvents(greenDiv, 0));
    goog.style.showElement(greenDiv, true);
  }

  function testLeftClick() {
    var coords = new goog.math.Coordinate(5, 5);
    var mouse = new bot.Mouse();
    mouse.move(redDiv, coords);
    mouse.pressButton(bot.Mouse.Button.LEFT);
    mouse.releaseButton();

    assertEvents(
      mouseoverAndMoveEvents(redDiv, 0),
      mousedownEvents(redDiv, b({ie: 1, wk: 0, ff: 0})),
      mouseupEvents(redDiv, b({ie: 1, wk: 0, ff: 0})),
      goog.events.EventType.CLICK, redDiv, 0
    );
  }

  function testNoLeftClickWhenReleasedOverOtherElement() {
    var coords = new goog.math.Coordinate(5, 5);
    var mouse = new bot.Mouse();
    mouse.move(greenDiv, coords);
    mouse.pressButton(bot.Mouse.Button.LEFT);
    mouse.move(redDiv, coords);
    mouse.releaseButton();

    // No click if we released on another element.
    assertEvents(
      mouseoverAndMoveEvents(greenDiv, 0),
      mousedownEvents(greenDiv, b({ie: 1, wk: 0, ff: 0})),
      mouseoutEvents(greenDiv, 0),
      mouseoverAndMoveEvents(redDiv, b({ie: 1, wk: 0, ff: 0})),
      mouseupEvents(redDiv, b({ie: 1, wk: 0, ff: 0}))
    );
  }

  function testRightClick() {
    var coords = new goog.math.Coordinate(5, 5);
    var mouse = new bot.Mouse();
    mouse.move(greenDiv, coords);
    mouse.pressButton(bot.Mouse.Button.RIGHT);
    mouse.move(redDiv, coords);
    mouse.releaseButton();

    // In https://chromium.googlesource.com/chromium/src/+/bafc23e1920cc799c03159c39e29878e26071db0
    // Chrome adopted the same values as IE and FF.
    var expectUninitializedButtons =
        goog.userAgent.product.CHROME && goog.userAgent.product.isVersion(60);
    var wkButtonData;
    if (expectUninitializedButtons) {
      wkButtonData = 0;
    } else {
      wkButtonData = 2;
    }

    // Right click triggers contextmenu even when released over another element.
    assertEvents(
      mouseoverAndMoveEvents(greenDiv, 0),
      mousedownEvents(greenDiv, 2),
      mouseoutEvents(greenDiv, b({ie: 2, wk: wkButtonData, ff: 0})),
      mouseoverAndMoveEvents(redDiv, b({ie: 2, wk: wkButtonData, ff: 0})),
      mouseupEvents(redDiv, 2),
      goog.events.EventType.CONTEXTMENU, redDiv, b({ie: 0, wk: 2, ff: 2})
    );
  }

  function testDoubleClick() {
    var coords = new goog.math.Coordinate(5, 5);
    var mouse = new bot.Mouse();
    mouse.move(redDiv, coords);
    mouse.pressButton(bot.Mouse.Button.LEFT);
    mouse.releaseButton();
    mouse.pressButton(bot.Mouse.Button.LEFT);
    mouse.releaseButton();

    assertEvents(
      mouseoverAndMoveEvents(redDiv, 0),
      mousedownEvents(redDiv, b({ie: 1, wk: 0, ff: 0})),
      mouseupEvents(redDiv, b({ie: 1, wk: 0, ff: 0})),
      goog.events.EventType.CLICK, redDiv, 0,
      mousedownEvents(redDiv, b({ie: 1, wk: 0, ff: 0})),
      mouseupEvents(redDiv, b({ie: 1, wk: 0, ff: 0})),
      goog.events.EventType.CLICK, redDiv, 0,
      goog.events.EventType.DBLCLICK, redDiv, 0
    );
  }

  function testNoDoubleClickWhenTheMouseMovesBetweenClicks() {
    var coords = new goog.math.Coordinate(5, 5);
    var mouse = new bot.Mouse();
    mouse.move(greenDiv, coords);
    mouse.pressButton(bot.Mouse.Button.LEFT);
    mouse.releaseButton();
    mouse.move(redDiv, coords);
    mouse.pressButton(bot.Mouse.Button.LEFT);
    mouse.releaseButton();

    assertEvents(
      mouseoverAndMoveEvents(greenDiv, 0),
      mousedownEvents(greenDiv, b({ie: 1, wk: 0, ff: 0})),
      mouseupEvents(greenDiv, b({ie: 1, wk: 0, ff: 0})),
      goog.events.EventType.CLICK, greenDiv, 0,
      mouseoutEvents(greenDiv, 0),
      mouseoverAndMoveEvents(redDiv, 0),
      mousedownEvents(redDiv, b({ie: 1, wk: 0, ff: 0})),
      mouseupEvents(redDiv, b({ie: 1, wk: 0, ff: 0})),
      goog.events.EventType.CLICK, redDiv, 0
    );
  }

  function testMoveOnSameElement() {
    var coords1 = new goog.math.Coordinate(5, 5);
    var coords2 = new goog.math.Coordinate(10, 5);
    var mouse = new bot.Mouse();
    mouse.move(greenDiv, coords1);
    mouse.move(greenDiv, coords2);

    assertEvents(
      mouseoverAndMoveEvents(greenDiv, 0),
      mousemoveEvents(greenDiv, 0)
    );
  }

  function testMoveToAnotherElement() {
    var coords = new goog.math.Coordinate(5, 5);
    var mouse = new bot.Mouse();
    mouse.move(greenDiv, coords);
    mouse.move(redDiv, coords);

    assertEvents(
      mouseoverAndMoveEvents(greenDiv, 0),
      mouseoutEvents(greenDiv, 0),
      mouseoverAndMoveEvents(redDiv, 0)
    );
  }

  function testFirstMoveHasNullRelated() {
    var mouse = new bot.Mouse();

    var fired = 0;

    goog.events.listenOnce(greenDiv, goog.events.EventType.MOUSEOVER,
        function(e) {
          fired++;
          // Even though bot.events.fire explicitly sets relatedTarget to null,
          // it sometimes becomes undefined in Firefox.
          assertEvaluatesToFalse(e.relatedTarget);
        });

    mouse.move(greenDiv, new goog.math.Coordinate(5, 5));

    assertEquals(1, fired);
  }

  function testSecondMoveHasRelatedSet() {
    var mouse = new bot.Mouse();
    mouse.move(greenDiv, new goog.math.Coordinate(5, 5));

    var fired = 0;
    var relatedTarget;
    goog.events.listen(redDiv, goog.events.EventType.MOUSEOVER,
        function(e) {
          fired++;
          // Catch the relatedTarget here, but check it below so any errors are
          // handled correctly in IE.
          relatedTarget = e.relatedTarget;
        });

    mouse.move(redDiv, new goog.math.Coordinate(5, 5));
    assertEquals('mouseover event not fired', 1, fired);
    assertNotNull(relatedTarget);
    assertNotEquals(redDiv, relatedTarget);
  }

  function testMoveMouseFromClosedWindowDoesNotError() {
    var mouse = new bot.Mouse();
    var coord = new goog.math.Coordinate(0, 0);
    var iframe = document.createElement('iframe');

    return new goog.Promise(function(loaded) {
      goog.events.listenOnce(iframe, 'load', loaded);
      iframe.src = 'testdata/blank_page.html';
      document.body.appendChild(iframe);
    }).then(function() {
      mouse.move(iframe.contentWindow.document.body, coord);
      return new goog.Promise(function(loaded) {
        goog.events.listenOnce(iframe, 'load', loaded);
        iframe.src = 'testdata/iframe_page.html';
      });
    }).then(function() {
      mouse.move(iframe.contentWindow.document.body, coord);
      document.body.removeChild(iframe);
    });
  }

  function testMoveMouseFiresAllEventsOnElementHiddenMidSequence() {
    var coords = new goog.math.Coordinate(5, 5);
    var mouse = new bot.Mouse();
    mouse.move(greenDiv, coords);
    goog.events.listen(greenDiv, 'mouseout', function() {
      goog.style.showElement(redDiv, false);
    });
    mouse.move(redDiv, coords);
    goog.style.showElement(redDiv, true);

    assertEvents(
      mouseoverAndMoveEvents(greenDiv, 0),
      mouseoutEvents(greenDiv, 0),
      mouseoverAndMoveEvents(redDiv, 0)
    );
  }

  function testScrollMouseZeroTicksThrowsError() {
    var mouse = new bot.Mouse();
    mouse.move(greenDiv, new goog.math.Coordinate(5, 5));
    assertThrows(function() {
      mouse.scroll(0);
    });
  }

  function testScrollMouseDown() {
    var mouse = new bot.Mouse();
    mouse.move(greenDiv, new goog.math.Coordinate(5, 5));
    mouse.scroll(1);

    if (goog.userAgent.GECKO) {
      assertEvents(
        mouseoverAndMoveEvents(greenDiv, 0),
        goog.events.EventType.MOUSEWHEEL, greenDiv, 0,
        goog.events.EventType.MOUSEPIXELSCROLL, greenDiv, 0
      );
    } else {
      assertEvents(
        mouseoverAndMoveEvents(greenDiv, 0),
        goog.events.EventType.MOUSEWHEEL, greenDiv, 0
      );
    }
  }

  function testScrollMouseUp() {
    var mouse = new bot.Mouse();
    mouse.move(greenDiv, new goog.math.Coordinate(5, 5));
    mouse.scroll(-2);

    if (goog.userAgent.GECKO) {
      assertEvents(
        mouseoverAndMoveEvents(greenDiv, 0),
        goog.events.EventType.MOUSEWHEEL, greenDiv, 0,
        goog.events.EventType.MOUSEPIXELSCROLL, greenDiv, 0,
        goog.events.EventType.MOUSEWHEEL, greenDiv, 0,
        goog.events.EventType.MOUSEPIXELSCROLL, greenDiv, 0
      );
    } else {
      assertEvents(
        mouseoverAndMoveEvents(greenDiv, 0),
        goog.events.EventType.MOUSEWHEEL, greenDiv, 0,
        goog.events.EventType.MOUSEWHEEL, greenDiv, 0
      );
    }
  }

  function testMsSetPointerCapture() {
    if (!bot.userAgent.IE_DOC_10) {
      return;
    }
    var coords = new goog.math.Coordinate(75, 5);
    var mouse = new bot.Mouse();
    mouse.move(captureDiv, coords);
    mouse.pressButton(bot.Mouse.Button.LEFT);
    mouse.move(greenDiv, coords);
    mouse.releaseButton();

    // The captureDiv will call msSetPointerCapture on MSPointerDown and as a
    // result, subsequent events should be fired on captureDiv instead of
    // greenDiv.
    assertEvents(
      mouseoverAndMoveEvents(captureDiv, 0),
      mousedownEvents(captureDiv, 0),
      mouseoutEvents(captureDiv, 0),
      mouseoverAndMoveEvents(captureDiv, 0),
      mouseupEvents(captureDiv, 0)
    );
  }

  function testClickDoesNotFireOnCapturedPointer() {
    if (!bot.userAgent.IE_DOC_10) {
      return;
    }
    var mouse = new bot.Mouse();
    mouse.move(innerCaptureDiv, new goog.math.Coordinate(5, 5));
    mouse.pressButton(bot.Mouse.Button.LEFT);
    mouse.move(innerCaptureDiv, new goog.math.Coordinate(6, 6));
    mouse.releaseButton();

    // The MSPointerDown event on innerCaptureDiv bubbles up to the
    // captureDiv element which calls msSetPointerCapture, so subsequent
    // events should fire on captureTarget except for the click event.
    assertEvents(
      mouseoverAndMoveEvents(innerCaptureDiv, 0),
      mousedownEvents(innerCaptureDiv, 0),
      mousemoveEvents(captureDiv, 0),
      mouseupEvents(captureDiv, 0),
      goog.events.EventType.CLICK, innerCaptureDiv, 0
    );
  }
</script>
</body>
</html>
